package controle;

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;

import javax.swing.JFileChooser;
import javax.swing.JFrame;

import lejos.pc.comm.NXTCommException;
import model.FisModel;
import model.RobotModel;
import vista.ErrorWindow;
import vista.InformationWindow;
import vista.MainWindow;
import vista.OpenFileWindow;
import vista.OptionsWindow;
import vista.SaveFileWindow;

public class RobotController {
	
	private static final String TRAVEL_SPEED_COMMAND = "VEL";
	private static final String ROTATION_SPEED_COMMAND = "VER";
	private static final String SPIN_COMMAND = "GIR";
	private static final String TURN_LEFT_COMMAND = "ESQ";
	private static final String TURN_RIGHT_COMMAND = "DIR";
	private static final String WALK_COMMAND = "AND";
	private static final String MEASURE_COMMAND = "MED";
	private static final String STOP_COMMAND = "PAR";
	private static final String BACK_COMMAND = "VOL";
	private static final String DISCONNECTION_COMMAND = "DIS";

	private static final String MSG_CONNECTED = "Connected.";
	private static final String MSG_DISCONNECTED = "Disconnected.";
	private static final String MSG_CONNECTION_ERROR = "Erro de Conexao";
	
	BluetoothController bluetoothController;
	MainWindow view;
	OptionsWindow viewOptions;
	OpenFileWindow openFileWindow;
	SaveFileWindow saveFileWindow;
	RobotModel robotModel;
	FisModel fisModel;
	private static final int ADJUSTMENT_HEAD_DISTANCE = 6;
	
	public RobotController () {
		
		view = new MainWindow();
		bluetoothController = new BluetoothController(view);
		robotModel = new RobotModel();
		fisModel = new FisModel();
		
		view.getMenuOptions().addActionListener(new ListenerMenuOptions());
		
		view.getConnectButton().addActionListener(new ListenerConnectButton());
		view.getDisconnectButton().addActionListener(new ListenerDisconnectButton());
		view.getRunButton().addActionListener(new ListenerRunButton());
		view.getStopButton().addActionListener(new ListenerStopButton());
		view.getBackButton().addActionListener(new ListenerBackButton());
		view.getOpenFileButton().addActionListener(new ListenerOpenFileButton());
		view.getCleanFileButton().addActionListener(new ListenerCleanFileButton());
		view.getMesureBotao().addActionListener(new ListenerMeasureButton());
		view.getCleanButton().addActionListener(new ListenerCleanButton());
		view.getSaveButton().addActionListener(new ListenerSaveButton());
		
		view.getSpeedField().setText(String.valueOf(robotModel.getTravelSpeed()));
		view.getWalkField().setText(String.valueOf(robotModel.getWalk()));
		view.getSpinField().setText(String.valueOf(robotModel.getSpin()));
		view.getTurnField().setText(String.valueOf(robotModel.getTurn()));
		view.getStepsField().setText(String.valueOf(robotModel.getSteps()));
		view.getFileField().setText("");
		
		view.getConnectButton().setEnabled(true);
		view.getConnectButton().setVisible(true);
		view.getDisconnectButton().setVisible(false);
		view.getDisconnectButton().setEnabled(false);
		view.getRunButton().setEnabled(false);
		view.getStopButton().setEnabled(false);
		view.getBackButton().setEnabled(false);
		view.getOpenFileButton().setEnabled(true);
		view.getCleanFileButton().setEnabled(true);
		view.getMesureBotao().setEnabled(false);
		
		view.getDescriptionPanel().setEditable(false);

		view.setVisible(true);
		
		view.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		
		viewOptions = new OptionsWindow();
		viewOptions.getCampoNomeRobo().setText(robotModel.getName());
		viewOptions.getCampoAnguloInicial().setText(String.valueOf(fisModel.getStartAngle()));
		viewOptions.getCampoPosicaoXInicial().setText(String.valueOf(fisModel.getStartPositionX()));
		viewOptions.getCampoPosicaoYInicial().setText(String.valueOf(fisModel.getStartPositionY()));
		viewOptions.getSaveButton().addActionListener(new ListenerSaveOptionsButton());
		
		openFileWindow = new OpenFileWindow(view);
		openFileWindow.getFileDiretories().setDialogTitle("Open");
		openFileWindow.getFileDiretories().setMultiSelectionEnabled(false);
		
		saveFileWindow = new SaveFileWindow(view);
		saveFileWindow.getFileDiretories().setDialogTitle("Save");
	}
	
	public static void main(String[] args){
		new RobotController();
	}
	
	class ListenerMenuOptions implements ActionListener {

		@Override
		public void actionPerformed(ActionEvent e) {
			viewOptions.setVisible(true);
		}
	}

	/**
	 * Inicializa a conexao com o Robo quando pressionado o botao correspondende, utilizando o BluetoothController
	 * @author daniel
	 *
	 */
	class ListenerConnectButton implements ActionListener {
		
		@Override
		public void actionPerformed(ActionEvent arg0) {
			try {
				bluetoothController.connect(robotModel.getName());
				printToPanel(MSG_CONNECTED);
				
				view.getConnectButton().setEnabled(false);
				view.getConnectButton().setVisible(false);
				view.getDisconnectButton().setVisible(true);
				view.getDisconnectButton().setEnabled(true);
				view.getRunButton().setEnabled(true);
				view.getStopButton().setEnabled(true);
				view.getBackButton().setEnabled(true);
				view.getOpenFileButton().setEnabled(true);
				view.getCleanFileButton().setEnabled(true);
				view.getMesureBotao().setEnabled(true);
			} catch (NXTCommException e) {
				new ErrorWindow(view, e.getClass().getName(), e.getMessage());
			}
		}
	}
	
	/**
	 * Encerra a conexao com o Robo quando pressionado o botao correspondende, utilizando o BluetoothControllers
	 *
	 */
	class ListenerDisconnectButton implements ActionListener {

		@Override
		public void actionPerformed(ActionEvent arg0) {
			try {
				bluetoothController.sendMessage(DISCONNECTION_COMMAND + "\n");
				bluetoothController.closeConnection();
				printToPanel(MSG_DISCONNECTED);
				
				view.getConnectButton().setEnabled(true);
				view.getConnectButton().setVisible(true);
				view.getDisconnectButton().setEnabled(false);
				view.getDisconnectButton().setVisible(false);
				view.getRunButton().setEnabled(false);
				view.getStopButton().setEnabled(false);
				view.getBackButton().setEnabled(false);
				view.getOpenFileButton().setEnabled(false);
				view.getCleanFileButton().setEnabled(false);
				view.getMesureBotao().setEnabled(false);
			} catch (IOException e) {
				new ErrorWindow(view, MSG_CONNECTION_ERROR, e.getMessage());
			}
		}
	}
	
	/**
	 * Limpa a área de descrição
	 *
	 */
	class ListenerCleanButton implements ActionListener {
		
		@Override
		public void actionPerformed(ActionEvent arg0) {
			view.getDescriptionPanel().setText("");
		}
	}
	
	/**
	 * Salva os dados da área de descrição
	 *
	 */
	class ListenerSaveButton implements ActionListener {
		
		@Override
		public void actionPerformed(ActionEvent arg0) {
			int returnVal = saveFileWindow.getFileDiretories().showSaveDialog(view);
			
			if (returnVal == JFileChooser.APPROVE_OPTION) {
				File file = saveFileWindow.getFileDiretories().getSelectedFile();
				String fileName = file.getAbsolutePath() + (file.getName().endsWith(".txt") ? "" : ".txt");
				try {
					saveFile(fileName, view.getDescriptionPanel().getText());
					new InformationWindow(view, "Salvo", "Arquivo criado com sucesso!\n\nLocal:\n" + fileName);
				} catch (IOException e) {
					new ErrorWindow(view, e.getClass().getName(), e.getMessage());
				}
	        }
		}
	}
	
	private void adjustSpeed(int travelSpeed, int rotateSpeed) {
		bluetoothController.sendMessage(TRAVEL_SPEED_COMMAND + "\n", travelSpeed);
		bluetoothController.sendMessage(ROTATION_SPEED_COMMAND + "\n", rotateSpeed);
	}
	
	private void saveFile(String fileName, String data) throws IOException {
		
		FileWriter fstream = new FileWriter(fileName);
		BufferedWriter out = new BufferedWriter(fstream);
		out.write(data);
		out.close();
	}
	
	/**
	 * Realiza a acao principal, que pode ser seguir o FIS, ou acoes simples, conforme o parametro do arquivo esta vazio ou nao
	 * @author daniel
	 *
	 */
	class ListenerRunButton implements ActionListener {
		
		@Override
		public void actionPerformed(ActionEvent arg0) {
			loadModel();
			adjustSpeed(robotModel.getTravelSpeed(), robotModel.getRotateSpeed());
			
			if (fisModel.getFile() != null) {
				runFIS();
			} else {
				runSimple();
			}
		}
		
	}
	
	/**
	 * Executa a acao de voltar quando o botao correspondente e pressionado
	 * @author daniel
	 *
	 */
	class ListenerBackButton implements ActionListener {
		
		@Override
		public void actionPerformed(ActionEvent arg0) {
			
			loadModel();
			bluetoothController.sendMessage(BACK_COMMAND + "\n", robotModel.getWalk());
			try {
				// TODO Tratar os valores recebidos
				bluetoothController.readInt();
				//bluetoothController.readInt();
			} catch (IOException e) {
				new ErrorWindow(view, MSG_CONNECTION_ERROR, e.getMessage());
			}
		}		
	}	
	
	/**
	 * Para o robo quando o botao correspondente e pressionado
	 * @author daniel
	 *
	 */
	class ListenerStopButton implements ActionListener {
		
		@Override
		public void actionPerformed(ActionEvent arg0) {
			loadModel();

			// TODO Consertar o parametro - nao temos o campo apropriado
			// deveria ser o tempo ate parar
			bluetoothController.sendMessage(STOP_COMMAND + "\n", 0);
			try {
				// TODO Tratar retorno
				bluetoothController.readInt();
				//bluetoothController.readInt();
			} catch (IOException e) {
				new ErrorWindow(view, MSG_CONNECTION_ERROR, e.getMessage());
			}	
			
		}		
	}	
	
	/**
	 * Seleciona um arquivo e prepara-o para ser usado pelo FIS
	 * @author daniel
	 *
	 */
	class ListenerOpenFileButton implements ActionListener {
		
		@Override
		public void actionPerformed(ActionEvent arg0) {
			int returnVal = openFileWindow.getFileDiretories().showOpenDialog(view);
			
			if (returnVal == JFileChooser.APPROVE_OPTION) {
				File file = openFileWindow.getFileDiretories().getSelectedFile();
		        view.getFileField().setText(file.getAbsolutePath());
		        fisModel.setFile(file);
	        }
		}		
	}
	
	/**
	 * Deseleciona um arquivo.
	 *
	 */
	class ListenerCleanFileButton implements ActionListener {
		
		@Override
		public void actionPerformed(ActionEvent arg0) {
			view.getFileField().setText("");
			fisModel.setFile(null);
		}		
	}
	
	/**
	 * Mede a distancia do robo para um obstaculo quando o botao correspondente e pressionado
	 * @author daniel
	 *
	 */
	class ListenerMeasureButton implements ActionListener {
		
		@Override
		public void actionPerformed(ActionEvent arg0) {
			loadModel();
			
			int distance;
			try {
				//Consumindo retorno
				distance = measureDistance();
				printToPanel("Distance: " + distance);
			} catch (IOException e) {
				new ErrorWindow(view, MSG_CONNECTION_ERROR, e.getMessage());
			}
		}		
	}
	
	/**
	 * Salva as opções
	 *
	 */
	class ListenerSaveOptionsButton implements ActionListener {
		
		@Override
		public void actionPerformed(ActionEvent arg0) {
			robotModel.setName(viewOptions.getCampoNomeRobo().getText());
			fisModel.setStartAngle(Integer.valueOf(viewOptions.getCampoAnguloInicial().getText()));
			fisModel.setStartPositionX(Integer.valueOf(viewOptions.getCampoPosicaoXInicial().getText()));
			fisModel.setStartPositionY(Integer.valueOf(viewOptions.getCampoPosicaoYInicial().getText()));
		}
	}
	
	/**
	 * Copia os valores da vista para o modelo para armazenagem 
	 * 
	 */
	private void loadModel () {
		robotModel.setTravelSpeed(Integer.parseInt((view.getSpeedField().getText())));
		robotModel.setWalk(Integer.parseInt(view.getWalkField().getText()));
		robotModel.setSpin(Integer.parseInt(view.getSpinField().getText()));
		robotModel.setTurn(Integer.parseInt(view.getTurnField().getText()));
		robotModel.setSteps(Integer.parseInt(view.getStepsField().getText()));
	}
	
	/**
	 * Executa o FIS presente no arquivo correspondente fornecido nos parametros pela quantidade de passos nos parametros
	 */
	public void runFIS() {
		try {
			robotModel.setPositionX(fisModel.getStartPositionX());
			robotModel.setPositionY(fisModel.getStartPositionY());
			robotModel.setCurrentAngle(fisModel.getStartAngle());
			int i = 0;
			while(i < robotModel.getSteps()) {
				int distance;
				float walked = 0, spun = 0;
				
				distance = measureDistance();
				printToPanel("Step: " + (i + 1));
				printToPanel(("Position Y: " + String.valueOf(robotModel.getPositionY())));
				printToPanel(("Position X: " + String.valueOf(robotModel.getPositionX())));
				printToPanel("Angle: " + String.valueOf(robotModel.getCurrentAngle()));
				printToPanel("Distance: " + String.valueOf(distance));
				
				// Aqui a biblioteca fuzzy calcula qual o angulo a desviar (distance * 6) / 10
				int angleVariation = (int) Math.round(Fuzzy.avalia(distance, robotModel.getCurrentAngle(), robotModel.getPositionY()));
				printToPanel("Variation: " + angleVariation);
				
				// Aqui ele gira
				robotModel.setSpin(angleVariation);
				spun = spin();
				// nao precisa mais de ajuste
				
				// Aqui ele anda
				walked = walk();

				printToPanel("Real Variation: " + String.valueOf(spun));
				printToPanel("Really Walked: " + String.valueOf(walked));
				printToPanel("\n");
				
				robotModel.setCurrentAngle(robotModel.getCurrentAngle() + Math.round(spun));
				
				// Aqui eu atualizo a posicao prevista
				robotModel.setPositionX(robotModel.getPositionX() 
						+ Math.round(walked) * Math.cos(robotModel.getCurrentAngle() * Math.PI / 180));
				robotModel.setPositionY(robotModel.getPositionY() 
						+ Math.round(walked) * Math.sin(robotModel.getCurrentAngle() * Math.PI / 180));
				
				i++;				
			}	
		} catch (IOException e) {
			new ErrorWindow(view, MSG_CONNECTION_ERROR, e.getMessage());
		}
	}
	
	/**
	 * Executa uma caminhada de tamanho, um giro e uma virada, com parametros definidos na vista
	 */
	public void runSimple() {
		float x;
		for (int i = 0; i < robotModel.getSteps(); i++) {
			if (robotModel.getWalk() != 0) {
				x = walk();
				printToPanel("Walked " + x);
			}
			if (robotModel.getTurn() != 0) {
				x = turn();
				printToPanel("Turned " + x);
			}
			if (robotModel.getSpin() != 0) {
				x = spin();
				printToPanel("Spun " + x);
			}
		}
	}
	
	/**
	 * O robo gira em torno do proprio eixo, por tantos graus quanto definido na vista
	 * valores positivos vão para a esquerda, negativos para a direita
	 */
	private float spin() {
		float spun  = 0;
		
		bluetoothController.sendMessage(SPIN_COMMAND + "\n", robotModel.getSpin());
		try {
			spun = bluetoothController.readFloat();
		} catch (IOException e) {
			e.printStackTrace();
		}
		
		return spun;
	}
	
	/**
	 * O robo vira em torno de um eixo externo (a roda interna quanto à direção), por tantos graus quanto definido na vista
	 * valores positivos vão para a esquerda, negativos para a direita
	 */
	private float turn() {
		String command;
		int angle;
		float spun  = 0;
		
		if (robotModel.getTurn() > 0) {
			command = TURN_LEFT_COMMAND + "\n";
			angle = robotModel.getTurn();
		} else {
			command = TURN_RIGHT_COMMAND + "\n";
			angle = robotModel.getTurn();
		}
		bluetoothController.sendMessage(command, angle);
		try {
			spun = bluetoothController.readFloat();
		} catch (IOException e) {
			new ErrorWindow(view, MSG_CONNECTION_ERROR, e.getMessage());
		}
		
		return spun;
	}
	
	/**
	 * O robo anda para frente por uma distancia determinada na vista
	 */
	private float walk() {
		float walked = 0;
		bluetoothController.sendMessage(WALK_COMMAND + "\n", robotModel.getWalk());
		try {
			// TODO Valores que o robo retorna, por enquanto sem uso
			//bluetoothController.readInt();
			//bluetoothController.readInt();
			walked = bluetoothController.readFloat();
		} catch (IOException e) {
			new ErrorWindow(view, MSG_CONNECTION_ERROR, e.getMessage());
		}
		
		return walked;
	}
	
	/**
	 * Mede a distancio usando o sensor de ultrasondas do robo
	 * Note que o valor possui uma constante para consertar a diferenca entre a frente do robo e a posicao do sensor
	 * @return A distancia
	 * @throws IOException
	 */
	private int measureDistance() throws IOException {
		bluetoothController.sendMessage(MEASURE_COMMAND + "\n");
		
		return bluetoothController.readInt() - ADJUSTMENT_HEAD_DISTANCE;
	}
	
	/**
	 * Imprime para o painel lateral da vista
	 * @param arg
	 */
	private void printToPanel(String arg) {
		String txt = view.getDescriptionPanel().getText();
		view.getDescriptionPanel().setText(txt + arg + "\n");
		System.out.println(arg);
	}
}
